require 'core_mods'

#
# this is a utility object, using String for storage
# it is useful for representing binary data
#
class BinStr
  # these next 5 constants may be relied upon by other modules

  # specified constants
  ELEMENTS_PER_BYTE = 1
  BITS_PER_BYTE = 8

  # derived constants
  BYTE_MAGNITUDE = 2**BITS_PER_BYTE
  MAX_INTEGER_PER_BYTE = BYTE_MAGNITUDE - 1
  MAX_INTEGER_PER_ELEMENT = 
    (MAX_INTEGER_PER_BYTE + 1) ** (1 / ELEMENTS_PER_BYTE) - 1

  # these are used only for hex string conversions (e.g. 'abcd' => "\xab\xcd")
  BYTE_ALIGN_FRONT = true
  IGNORE_NIBBLES = false
  NIBBLE_FILL = '0'


  SPACED_HEX = "%02x "
  UNPRINTABLE_LABELS = ['NUL', 'SOH', 'STX', 'ETX', 'EOT', 'ENQ', 'ACK', 'BEL',
                        'BS',  'HT',  'LF',  'VT',  'FF',  'CR',  'SO',  'SI', 
                        'DLE', 'DC1', 'DC2', 'DC3', 'DC4', 'NAK', 'SYN', 'ETB',
                        'CAN', 'EM',  'SUB', 'ESC', 'FS',  'GS',  'RS',  'US']

  NonP = {}
  UNPRINTABLE_LABELS.each_with_index { |l, i|
    NonP[l] = i.chr
  }
  NonP['DEL'] = "\x7f"

  def self.new_with_hex(hex)
    b = BinStr.new
    b.hex = hex
    b
  end

  # bracket and label unprintables
  #
  def self.display(str)
    str = str.dup
    NonP.each { |k,v| str.gsub!(v, "<#{k}>") }
    str
  end

  # longitudinal redundancy check
  #
  def self.lrc(str)
    warn 'LRC calculation included <STX>' if str[0].chr == NonP['STX']
    lrc = 0
    str.each_byte { |b| lrc = lrc ^ b }
    lrc.chr
  end

  def self.bytes_per(num_elements)
    num_elements
  end

  def self.elements_per(num_bytes)
    num_bytes
  end

  # evaluate a byte array as one big integer
  # 
  def self.scalarize(int_ary, bytes_per_int = 1)
    tmp = 0
    int_ary.reverse.each_with_index { |int, index|
      tmp += int * BYTE_MAGNITUDE ** (index * bytes_per_int)
    }
    tmp
  end

  # nicely-formatted hex dump
  #
  def self.bytes(str, cols = 3, per_col = 8)
    count = 1
    tmp = ''
    str.each_byte do |b|
      tmp << sprintf(SPACED_HEX, b)
      tmp << ' ' if count % per_col == 0
      tmp << "\n" if count % (per_col * cols) == 0
      count += 1
    end
    tmp.chop
  end

  # bit string in network byte order
  #
  def self.bits(str, sep = ' ')
    self.to_byte_array(str).map { |int|
      self.to_bit_str(int.chr)
    }.join(sep)
  end

  # return an even number of characters
  #
  def self.align_hex(str, byte_align_front = nil,
                     ignore_nibbles = nil,
                     nibble_fill = nil)
    #raise "bad hex: #{str}" if !str.match(/^[A-Fa-f0-9]*$/)
    return str if str.length.even?

    # str's length is odd
    byte_align_front = BYTE_ALIGN_FRONT if byte_align_front.nil?
    ignore_nibbles = IGNORE_NIBBLES if ignore_nibbles.nil?
    nibble_fill = NIBBLE_FILL if nibble_fill.nil?

    if byte_align_front
      if ignore_nibbles
        str.chop
      else
        str + nibble_fill
      end
    else
      if ignore_nibbles
        str[1..-1]
      else
        nibble_fill + str
      end
    end
  end

  def self.to_hex_str(str)
    str.unpack('H*').first
  end

  def self.from_hex_str(str, baf = nil, ign = nil, nf = nil)
    str = str.gsub(/\W/, '').gsub(/0x/, '')
    tmp = self.align_hex(str, baf, ign, nf).scan(/../)
    tmp.pack('H2' * tmp.length)
  end

  def self.to_bit_str(str)
    str.unpack('B*').first
  end

  def self.from_bit_str(str)
    [str].pack('B*')
  end

  def self.to_byte_array(str)
    str.unpack('C*')
  end

  def self.from_byte_array(ary)
    ary.pack('C' * ary.length)
  end

  def self.to_int(str, bytes_per_int = 1)
    str = str.rjust(bytes_per_int, 0.chr)

    case bytes_per_int
    when 1
      str.unpack('C*').first
    when 2
      str.unpack('n*').first
    when 4
      str.unpack('N*').first
    when 8, 16, 32, 64
      next_bpi = bytes_per_int / 2
      first_half = str.slice!(0, next_bpi)

      self.to_int(first_half, next_bpi) * BYTE_MAGNITUDE ** next_bpi + 
        self.to_int(str, next_bpi)
    else
      raise "unsupported bytes_per_int: #{bytes_per_int}"
    end
  end

  def self.from_int(int, bytes_per_int = 1)
    case bytes_per_int
    when 1
      [int].pack('C')
    when 2
      [int].pack('n')
    when 4
      [int].pack('N')
    when 8, 16, 32, 64
      next_bpi = bytes_per_int / 2
      a = int / 256 ** next_bpi
      b = int - a * 256 ** next_bpi
      self.from_int(a, next_bpi) + self.from_int(b, next_bpi)
    else
      raise "unsupported bytes_per_int: #{bytes_per_int}"
    end
  end

  def initialize(contents = '')
    @contents = contents
    @bytes_per_int = 2
    @byte_align_front = BYTE_ALIGN_FRONT
    @ignore_nibbles = IGNORE_NIBBLES
    @nibble_fill = NIBBLE_FILL
  end

  attr_accessor(:contents, :bytes_per_int, 
                :byte_align_front, :ignore_nibbles, :nibble_fill)

  def hex
    self.class.to_hex_str(@contents)
  end

  def hex=(input)
    @contents = self.class.from_hex_str(input, @byte_align_front, 
                                        @ignore_nibbles, @nibble_fill)
  end

  def int
    self.class.to_int(@contents, @bytes_per_int)
  end

  def int=(input)
    @contents = self.class.from_int(input, @bytes_per_int)
  end

  def ary
    self.class.to_byte_array(@contents)
  end

  def ary=(input)
    @contents = self.class.from_byte_array(input)
  end

  def display
    @contents
  end
  alias_method :to_s, :display

  def bytes
    self.class.bytes @contents
  end

  def bits
    self.class.bits @contents
  end
end
